/**
 ** This file is part of FK Klistret Extensions. FK Klistret Extensions 
 ** is free software: you can redistribute it and/or modify it under the 
 ** terms of the GNU General Public License as published by the Free 
 ** Software Foundation, either version 3 of the License, or (at your option) 
 ** any later version.

 ** FK Klistret Extensions is distributed in the hope that it will be 
 ** useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 ** General Public License for more details. You should have received a
 ** copy of the GNU General Public License along with FK Klistret Extensions. 
 ** If not, see <http://www.gnu.org/licenses/>
 */
package se.fk.klistret.console.ui;

import java.util.Collections;
import java.util.List;
import java.util.Vector;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

/**
 * 2011-07-11, The comments below are from 2005 and the code is has since
 * upgraded to the Ant 1.8 platform. The patterns and design of this project
 * should not be reused since they are not considered of any quality. The code
 * was written quickly without long term concerns.
 * <p>
 * 
 * 2005, TextMenu manages the display of TextMenu items, construction of text
 * menu tree, and processing of user commands. The TextMenu provides a menu like
 * listing of text items which can be selected by index and allowing for
 * specific free/expression text to be entered by the user.
 * 
 * @author Matthew Young
 */
public class TextMenu extends TextMenuItem implements TextMenuContainer {

	/**
	 * Vector of TextMenuItem objects (child items)
	 */
	private List<TextMenuItem> items = Collections
			.synchronizedList(new Vector<TextMenuItem>());

	/**
	 * Exit expressions (exit,quit,bye,back)
	 */
	private String exit_expression = new String(
			"exit|e|quit|q|bye|b|back|\\.\\.");

	/**
	 * All expression firing events for all child items
	 */
	private String all_expression = new String("all|a");

	/**
	 * Confirm expression to toggle between activating/deactiving a confirmation
	 * prompt prior to firing an event.
	 */
	private String confirm_expression = new String("confirm|c");

	/**
	 * Help expression to display help text
	 */
	private String help_expression = new String("help|h");

	/**
	 * User defined expression to be processed by the implementation of the
	 * actionPerformed method (client side).
	 */
	private String prompt_expression;

	/**
	 * Menu header
	 */
	private String header = new String("");

	/**
	 * Menu footer
	 */
	private String footer = new String("");

	/**
	 * Default prompt
	 */
	private String prompt = new String(" prompt > ");

	/**
	 * Help text
	 */
	private String help = new String("");

	/**
	 * Automatically exit from menu after event processing (default true)
	 */
	private boolean autoexit = true;

	/**
	 * Automatically display menu content after event processing (default true)
	 */
	private boolean autodisplay = true;

	/**
	 * Automatically prompt for confirmation prior to event processing (default
	 * true)
	 */
	private boolean autoconfirm = true;

	/**
	 * Set autodisplay
	 * 
	 * @param autodisplay
	 *            <code>boolean</code>
	 * 
	 */
	public void setAutoDisplay(boolean autodisplay) {
		this.autodisplay = autodisplay;
	}

	/**
	 * Set autoexit
	 * 
	 * @param autoexit
	 *            <code>boolean</code>
	 * 
	 */
	public void setAutoExit(boolean autoexit) {
		this.autoexit = autoexit;
	}

	/**
	 * Toggle autoconfirm
	 * 
	 */
	public void setAutoConfirm() {

		if (autoconfirm) {
			autoconfirm = false;
			System.out.println("Confirm off for " + this.getLabel());
		} else {
			autoconfirm = true;
			System.out.println("Confirm on for " + this.getLabel());
		}

	}

	/**
	 * Set menu header
	 * 
	 * @param header
	 *            <code>String</code>
	 * 
	 */
	public void setHeader(String header) {
		this.header = header;
	}

	/**
	 * Set menu footer
	 * 
	 * @param footer
	 *            <code>String</code>
	 * 
	 */
	public void setFooter(String footer) {
		this.footer = footer;
	}

	/**
	 * Set menu prompt
	 * 
	 * @param prompt
	 *            <code>String</code>
	 * 
	 */
	public void setPrompt(String prompt) {
		this.prompt = prompt;
	}

	/**
	 * Set help text
	 * 
	 * @param help
	 *            <code>String</code>
	 * 
	 */
	public void setHelp(String help) {
		this.help = help;
	}

	/**
	 * Set prompt expression to active client specified text/commands to be
	 * matched and fire events.
	 * 
	 * @param expression
	 *            <code>String</code>
	 * 
	 */
	public void setPromptExpression(String expression) {
		this.prompt_expression = expression;
	}

	/**
	 * Return number of child items
	 * 
	 * @return <code>int</code>
	 * 
	 */
	public synchronized int getItemCount() {
		return items.size();
	}

	public List<TextMenuItem> copy() {
		List<TextMenuItem> other = new Vector<TextMenuItem>(items.size());

		for (TextMenuItem item : items)
			other.add(item);

		return other;
	}

	/**
	 * Return a specific child item
	 * 
	 * @param index
	 *            <code>int</code>
	 * 
	 * @return <code>TextMenuItem</code>
	 * 
	 */
	public synchronized TextMenuItem getItem(int index) {
		return (TextMenuItem) items.get(index);
	}

	/**
	 * Add a TextMenuItem
	 * 
	 * @param item
	 *            <code>TextMenuItem</code>
	 * 
	 * @return <code>TextMenuItem</code>
	 * 
	 */
	public synchronized TextMenuItem add(TextMenuItem item) {

		if (item.getParent() != null)
			item.getParent().remove(item);

		items.add(item);

		item.setParent(this);

		return item;
	}

	/**
	 * Remove TextMenuItem
	 * 
	 * @param component
	 *            <code>TextMenuComponent</code>
	 * 
	 */
	public synchronized void remove(TextMenuComponent component) {
		int index = items.indexOf(component);

		if (index >= 0)
			remove(index);
	}

	/**
	 * Remove TextMenuItem from a specific index
	 * 
	 * @param index
	 *            <code>int</code>
	 * 
	 */
	public synchronized void remove(int index) {
		TextMenuItem item = getItem(index);

		items.remove(index);

		item.setParent(null);
	}

	/**
	 * Remove all TextMenuItem
	 * 
	 */
	public synchronized void removeAll() {
		int count = getItemCount();

		for (int index = count - 1; index >= 0; index--) {
			remove(index);
		}
	}

	/**
	 * Confirmation prior to firing an event
	 * <p>
	 * 
	 * @param item
	 *            <code>TextMenuItem</code>
	 * 
	 * @return <code>boolean</code>
	 * 
	 */
	private boolean confirm(TextMenuItem item) {

		BufferedReader input = new BufferedReader(new InputStreamReader(
				System.in));

		if (item.getConfirmPrompt() == null || autoconfirm == false)
			return true;

		try {
			for (;;) {
				System.out.print(item.getConfirmPrompt());
				String line = input.readLine().trim();

				if (line.matches("yes|y"))
					return true;
				if (line.matches("no|n"))
					return false;
			}
		} catch (IOException ie) {
			throw new RuntimeException(
					"Error reading confirmation prompt input. "
							+ ie.getMessage());
		}

	}

	/**
	 * Process menu commands for TextMenu objects (usually for display purposes
	 * dvs. receive information before displaying menu items or creating menu
	 * items prior to display)
	 * <p>
	 * <br>
	 * Note: </br> Display logic.
	 * 
	 * @param line
	 *            <code>String</code>
	 * 
	 * @return <code>boolean</code>
	 * 
	 */
	private boolean processTextMenu(String line) {

		try {
			int value = Integer.parseInt(line);

			if (getItemCount() != 0) {

				if (value >= 1 && value <= getItemCount()) {

					TextMenuItem item = getItem(value - 1);

					if (item instanceof TextMenuContainer) {
						item.processEvent(TextMenuEvent.ACTION_ITEM);

						((TextMenu) item).display();
						((TextMenu) item).displayPrompt();

						return true;
					}

				}

			}
		} catch (NumberFormatException nfe) {
		}

		return false;

	}

	/**
	 * Extends processTextMenuItem method to allow the user to type the label
	 * name for the TextMenuItem rather than an index number.
	 * <p>
	 * 
	 * @param line
	 *            <code>String</code>
	 * 
	 * @return <code>int</code>
	 * 
	 */
	private int getTextMenuItemIndex(String line) {

		for (int index = 0; index < getItemCount(); index++)
			if (line.equals(((TextMenuItem) getItem(index)).getLabel()))
				return index + 1;

		return Integer.parseInt(line);

	}

	/**
	 * Process menu commands for TextMenuItem objects (handles specific actions
	 * for menu item for example when a user selected a specific item).
	 * <p>
	 * <br>
	 * Note: </br> Display logic.
	 * 
	 * @param line
	 *            <code>String</code>
	 * 
	 * @return <code>boolean</code>
	 * 
	 */
	private boolean processTextMenuItem(String line) {

		try {
			int value = getTextMenuItemIndex(line);

			if (getItemCount() != 0) {

				if (value >= 1 && value <= getItemCount()) {

					TextMenuItem item = getItem(value - 1);

					if (!(item instanceof TextMenuContainer)) {
						if (confirm(item))
							item.processEvent(TextMenuEvent.ACTION_ITEM);

						if (getParent() == null || autoexit == false) {
							if (autodisplay)
								display();
							displayPrompt();
						} else {
							((TextMenu) getParent()).display();
							((TextMenu) getParent()).displayPrompt();
						}

						return true;
					}

				}

			}
		} catch (NumberFormatException nfe) {
		}

		return false;

	}

	/**
	 * Process menu commands for TextMenu objects (user defined text/commands at
	 * handled at the menu level).
	 * <p>
	 * <br>
	 * Note: </br> Display logic.
	 * 
	 * @param line
	 *            <code>String</code>
	 * 
	 * @return <code>boolean</code>
	 * 
	 */
	private boolean processPromptExpression(String line) {

		if (prompt_expression != null && line.matches(prompt_expression)) {
			this.processEvent(TextMenuEvent.ACTION_EXPRESSION, line);

			if (getParent() == null || autoexit == false) {
				if (autodisplay)
					display();
				displayPrompt();
			} else {
				((TextMenu) getParent()).display();
				((TextMenu) getParent()).displayPrompt();
			}

			return true;
		}

		return false;

	}

	/**
	 * Process blank line/carriage returns to display again menu content.
	 * <p>
	 * 
	 * @param line
	 *            <code>String</code>
	 * 
	 * @return <code>boolean</code>
	 * 
	 */
	private boolean processBlank(String line) {

		if (line.matches("")) {
			display();

			return true;
		}

		return false;

	}

	/**
	 * Process help by printing text.
	 * <p>
	 * 
	 * @param line
	 *            <code>String</code>
	 * 
	 * @return <code>boolean</code>
	 * 
	 */
	private boolean processHelp(String line) {

		if (line.matches(help_expression)) {
			System.out.println(help);

			return true;
		}

		return false;

	}

	/**
	 * Process confirmation expression by toggeling the autoconfirm variable
	 * <p>
	 * 
	 * @param line
	 *            <code>String</code>
	 * 
	 * @return <code>boolean</code>
	 * 
	 */
	private boolean processConfirm(String line) {

		if (line.matches(confirm_expression)) {
			setAutoConfirm();

			return true;
		}

		return false;

	}

	/**
	 * Process all expression to fire events for all menu items (works at the
	 * menu level)
	 * <p>
	 * <br>
	 * Note: </br> Display logic.
	 * 
	 * @param line
	 *            <code>String</code>
	 * 
	 * @return <code>boolean</code>
	 * 
	 */
	private boolean processAllExpression(String line) {

		if (line.matches(all_expression)) {

			List<TextMenuItem> copy = copy();
			for (TextMenuItem item : copy) {
				if (!(item instanceof TextMenuContainer))
					if (confirm(item))
						item.processEvent(TextMenuEvent.ACTION_ITEM);
			}

			if (getParent() == null || autoexit == false) {
				if (autodisplay)
					display();
				displayPrompt();
			} else {
				((TextMenu) getParent()).display();
				((TextMenu) getParent()).displayPrompt();
			}

			return true;

		}

		return false;

	}

	/**
	 * Process exit expression whereby the parent menu is displayed unless the
	 * current menu is root then the TextMenu framework is exited.
	 * <p>
	 * <br>
	 * Note: </br> Display logic.
	 * 
	 * @param line
	 *            <code>String</code>
	 * 
	 * @return <code>boolean</code>
	 * 
	 */
	private boolean processExitExpression(String line) {

		if (line.matches(exit_expression)) {

			if (getParent() == null)
				System.out.println("Exiting...");
			else {
				((TextMenu) getParent()).display();
				((TextMenu) getParent()).displayPrompt();
			}

			return true;

		}

		return false;

	}

	/**
	 * Prints header information
	 * <p>
	 * 
	 */
	private void displayHeader() {
		System.out.println(header);
	}

	/**
	 * Prints footer information
	 * <p>
	 * 
	 */
	private void displayFooter() {
		System.out.println(footer);
	}

	/**
	 * Prints menu label, header, content, and footer.
	 * <p>
	 * 
	 */
	private void display() {

		System.out.println("\n" + getLabel().toUpperCase());

		displayHeader();

		for (int index = 1; index <= getItemCount(); index++)
			System.out.println("[" + index + "] "
					+ getItem(index - 1).getLabel());

		displayFooter();

	}

	/**
	 * Prints display prompt
	 * <p>
	 * 
	 */
	private void displayPrompt() {

		BufferedReader input = new BufferedReader(new InputStreamReader(
				System.in));

		try {
			for (;;) {
				System.out.print(prompt);

				String line = input.readLine().trim();

				if (processTextMenu(line))
					break;

				if (processTextMenuItem(line))
					break;

				if (processAllExpression(line))
					break;

				if (processPromptExpression(line))
					break;

				if (processBlank(line))
					continue;

				if (processConfirm(line))
					continue;

				if (processHelp(line))
					continue;

				if (processExitExpression(line))
					break;

				System.out.println("Input (" + line
						+ ") matches no defined pattern.");
			}
		} catch (IOException ie) {
			throw new RuntimeException("Error reading prompt input. "
					+ ie.getMessage());
		}

	}

	/**
	 * Prints menu (only if root menu)
	 * <p>
	 * 
	 */
	public void setVisible() {
		if (getParent() == null) {
			display();

			displayPrompt();
		}
	}

	/**
	 * Constructs basic TextMenu
	 * <p>
	 * 
	 */
	public TextMenu() {
		this("");
	}

	/**
	 * Constructs TextMenu setting the display label/text
	 * <p>
	 * 
	 */
	public TextMenu(String label) {
		super(label);
	}

}
